package com.googlecode.bluetools.common.utils;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

/**
 * Contains non UI utility methods.
 * 
 * @author marcelop
 * @since 0.0.1
 */
public class CoreUtil {
	/**
	 * Returns the throwable's stack trace as a String.
	 * 
	 * @param throwable
	 * @return String
	 */
	public static String getStackTrace(Throwable throwable) {
		if (throwable == null)
			return null;

		ByteArrayOutputStream errorStream;
		PrintStream errorPS;

		errorStream = new ByteArrayOutputStream();
		errorPS = new PrintStream(errorStream);

		throwable.printStackTrace(errorPS);
		errorPS.flush();

		String stackTrace = errorStream.toString();

		try {
			errorStream.close();
		}
		catch (Exception e) {
		}

		return stackTrace;
	}

	/**
	 * Returns the Object value of a given object's field - or, using OO words, attribute. If the field is not available
	 * in the object's class, this method will look for it in the superclass hierarchy.
	 * 
	 * <p>
	 * If the value of <code>throwException</code> is true then any exception during the introspection process is thrown
	 * as a RuntimeExceptionDecorator. If it is false and there is an exception the method returns null.
	 * 
	 * @param object
	 * @param fileName
	 * @param throwException
	 * @return Object
	 * @throws RuntimeExceptionDecorator if there is an error and <code>throwException</code> is true.
	 * 
	 * @see Class#getDeclaredField(java.lang.String)
	 * @see Field#get(java.lang.Object)
	 */
	public static Object getObjectFieldValue(Object object, String fieldName, boolean throwException)
			throws RuntimeException {
		if ((object == null) || (fieldName == null))
			return null;

		RuntimeExceptionDecorator red = null;
		Field field = null;
		Class currentClass = object.getClass();
		while ((field == null) && (currentClass != null)) {
			try {
				field = currentClass.getDeclaredField(fieldName);
			}
			catch (Exception e) {
				if (throwException && (red == null))
					red = new RuntimeExceptionDecorator(e);

				currentClass = currentClass.getSuperclass();
			}
		}
		if (field == null) {
			if (red != null)
				throw red;

			return null;
		}

		try {
			field.setAccessible(true);
			return field.get(object);
		}
		catch (Exception e) {
			if (throwException)
				throw new RuntimeExceptionDecorator(e);
		}

		return null;
	}

	/**
	 * See {@link #invokeObjectMethod(Object, String, Class[], Object[], boolean)}. This method is a simple utility that
	 * creates the parameter type array based on the classes of the arguments.
	 * 
	 * @param object
	 * @param methodName
	 * @param arguments
	 * @param throwException
	 * @return Object
	 * @throws RuntimeException
	 */
	public static Object invokeObjectMethod(Object object, String methodName, Object[] arguments, boolean throwException)
			throws RuntimeException {
		Class[] parameterTypes = null;
		if (arguments != null) {
			parameterTypes = new Class[arguments.length];
			for (int i = 0, maxi = arguments.length; i < maxi; i++)
				parameterTypes[i] = arguments[i].getClass();
		}

		return invokeObjectMethod(object, methodName, parameterTypes, arguments, throwException);
	}

	/**
	 * Invokes an specific method of a given object. If the method is not available in the object's class, this method
	 * will look for it in the superclass hierarchy.
	 * 
	 * <p>
	 * If the Object's method has no arguments, callers can set <code>parameterType</code> and <code>argument</code> as
	 * null.
	 * 
	 * <p>
	 * If the value of <code>throwException</code> is true then any exception during the introspection process is thrown
	 * as a RuntimeExceptionDecorator. If it is false and there is an exception the method returns null.
	 * 
	 * @param object
	 * @param methodName
	 * @param parameterTypes
	 * @param arguments
	 * @param throwException
	 * @return An Object returned by the invoked method.
	 * @throws RuntimeException if there is an error and <code>throwException</code> is true.
	 * 
	 * @see Class#getDeclaredMethod(java.lang.String, java.lang.Class[]);
	 * @see Method#invoke(java.lang.Object, java.lang.Object[])
	 */
	public static Object invokeObjectMethod(Object object, String methodName, Class[] parameterTypes,
			Object[] arguments, boolean throwException) throws RuntimeException {
		if ((object == null) || (methodName == null))
			return null;

		if (parameterTypes == null)
			parameterTypes = new Class[0];

		if (arguments == null)
			arguments = new Object[0];

		RuntimeExceptionDecorator red = null;
		Method method = null;
		Class currentClass = object.getClass();
		while ((method == null) && (currentClass != null)) {
			try {
				method = currentClass.getDeclaredMethod(methodName, parameterTypes);
			}
			catch (Exception e) {
				if (throwException && (red == null))
					red = new RuntimeExceptionDecorator(e);

				currentClass = currentClass.getSuperclass();
			}
		}
		if (method == null) {
			if (red != null)
				throw red;

			return null;
		}

		try {
			method.setAccessible(true);
			return method.invoke(object, arguments);
		}
		catch (Exception e) {
			if (throwException)
				throw new RuntimeExceptionDecorator(e);
		}

		return null;
	}

	/**
	 * Removes the bit flag from the integer value returning the new value.
	 * 
	 * @param value
	 * @param flag
	 * @return int
	 */
	public static int removeBitFlag(int value, int flag) {
		return (~((~value) | flag));
	}

	/**
	 * Returns the integer value for the bits passed as a String. Example: 5 = fromBynaryString("101") and 10 =
	 * fromBynaryString("1010");
	 * 
	 * <p>
	 * Any character different than 1 is considered 0.
	 * 
	 * @param bits
	 * @return int
	 */
	public static int fromBynaryString(String bits) {
		int ret = 0;

		int length = bits.length();
		for (int i = length; i > 0; i--) {
			if (bits.charAt(i - 1) == '1')
				ret += Math.pow(2, (length - i));
		}

		return ret;
	}

	/**
	 * Changes the owner list by moving all the specified elements up. If one element is at the top, it is not moved.
	 * 
	 * @param owner
	 * @param elements
	 */
	public static void moveUp(List owner, List elements) {
		if ((owner == null) || owner.isEmpty() || (elements == null) || elements.isEmpty())
			return;

		int top = 0;
		for (Iterator i = elements.iterator(); i.hasNext();) {
			Object element = i.next();
			int index = owner.indexOf(element);
			if (index < 0)
				continue;

			if (index > top)
				swap(owner, index, index - 1);
			else
				top++;
		}
	}

	/**
	 * Changes the owner list by moving all the specified elements down. If one element is at the botton, it is not
	 * moved.
	 * 
	 * @param owner
	 * @param elements
	 */
	public static void moveDown(List owner, List elements) {
		if ((owner == null) || owner.isEmpty() || (elements == null) || elements.isEmpty())
			return;

		int botton = owner.size() - 1;
		for (ListIterator i = elements.listIterator(elements.size()); i.hasPrevious();) {
			Object element = i.previous();
			int index = owner.indexOf(element);
			if (index < 0)
				continue;

			if (index < botton)
				swap(owner, index, index + 1);
			else
				botton--;
		}
	}

	/**
	 * This method swaps two elements in a list identified by their indexes. The elements are remvoed from the list and
	 * them reinserted which is save when manipulating unique lists.
	 * 
	 * @param list
	 * @param index1
	 * @param index2
	 */
	public static void swap(List list, int index1, int index2) {
		if ((list == null) || (index1 == index2))
			return;

		int lastIndex = list.size() - 1;
		if ((index1 > lastIndex) || (index2 > lastIndex) || (index1 < 0) || (index2 < 0))
			return;

		if (index1 > index2) {
			int index = index2;
			index2 = index1;
			index1 = index;
		}

		Object o1 = list.get(index1);
		Object o2 = list.get(index2);

		list.remove(o1);
		list.remove(o2);

		list.add(index1, o2);
		list.add(index2, o1);
	}
}
